/**
 * 
 */
package org.mspring.platform.api.sitemap;

import static java.util.Calendar.HOUR_OF_DAY;
import static java.util.Calendar.MILLISECOND;
import static java.util.Calendar.MINUTE;
import static java.util.Calendar.SECOND;

import java.text.DateFormat;
import java.text.FieldPosition;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

/**
 * <p>
 * Formats and parses dates in the six defined W3C date time formats. These
 * formats are described in "Date and Time Formats", <a
 * href="http://www.w3.org/TR/NOTE-datetime"
 * >http://www.w3.org/TR/NOTE-datetime</a>.
 * </p>
 * 
 * <p>
 * The formats are:
 * 
 * <ol>
 * <li>YEAR: YYYY (eg 1997)
 * <li>MONTH: YYYY-MM (eg 1997-07)
 * <li>DAY: YYYY-MM-DD (eg 1997-07-16)
 * <li>MINUTE: YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00)
 * <li>SECOND: YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00)
 * <li>MILLISECOND: YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)
 * </ol>
 * 
 * Note that W3C timezone designators (TZD) are either the letter "Z" (for GMT)
 * or a pattern like "+00:30" or "-08:00". This is unlike RFC 822 timezones
 * generated by SimpleDateFormat, which omit the ":" like this: "+0030" or
 * "-0800".
 * </p>
 * 
 * <p>
 * This class allows you to either specify which format pattern to use, or (by
 * default) to automatically guess which pattern to use (AUTO mode). When
 * parsing in AUTO mode, we'll try parsing using each pattern until we find one
 * that works. When formatting in AUTO mode, we'll use this algorithm:
 * 
 * <ol>
 * <li>If the date has fractional milliseconds (e.g. 2009-06-06T19:49:04.45Z)
 * we'll use the MILLISECOND pattern
 * <li>Otherwise, if the date has non-zero seconds (e.g. 2009-06-06T19:49:04Z)
 * we'll use the SECOND pattern
 * <li>Otherwise, if the date is not at exactly midnight (e.g.
 * 2009-06-06T19:49Z) we'll use the MINUTE pattern
 * <li>Otherwise, we'll use the DAY pattern. If you want to format using the
 * MONTH or YEAR pattern, you must declare it explicitly.
 * </ol>
 * 
 * Finally note that, like all classes that inherit from DateFormat, <b>this
 * class is not thread-safe</b>. Also note that you can explicitly specify the
 * timezone to use for formatting using the {@link #setTimeZone(TimeZone)}
 * method.
 * 
 * @author Dan Fabulich
 * @see <a href="http://www.w3.org/TR/NOTE-datetime">Date and Time Formats</a>
 */
public class W3CDateFormat extends SimpleDateFormat {
    private static final long serialVersionUID = -5733368073260485802L;

    /** The six patterns defined by W3C, plus {@link #AUTO} configuration */
    public enum Pattern {
        /** "yyyy-MM-dd'T'HH:mm:ss.SSSZ" */
        MILLISECOND("yyyy-MM-dd'T'HH:mm:ss.SSSZ", true),
        /** "yyyy-MM-dd'T'HH:mm:ssZ" */
        SECOND("yyyy-MM-dd'T'HH:mm:ssZ", true),
        /** "yyyy-MM-dd'T'HH:mmZ" */
        MINUTE("yyyy-MM-dd'T'HH:mmZ", true),
        /** "yyyy-MM-dd" */
        DAY("yyyy-MM-dd", false),
        /** "yyyy-MM" */
        MONTH("yyyy-MM", false),
        /** "yyyy" */
        YEAR("yyyy", false),
        /** Automatically compute the right pattern to use */
        AUTO("", true);

        private final String pattern;
        private final boolean includeTimeZone;

        Pattern(String pattern, boolean includeTimeZone) {
            this.pattern = pattern;
            this.includeTimeZone = includeTimeZone;
        }
    }

    private final Pattern pattern;
    /** The GMT ("zulu") time zone, for your convenience */
    public static final TimeZone ZULU = TimeZone.getTimeZone("GMT");

    /** Build a formatter in AUTO mode */
    public W3CDateFormat() {
        this(Pattern.AUTO);
    }

    /** Build a formatter using the specified Pattern, or AUTO mode */
    public W3CDateFormat(Pattern pattern) {
        super(pattern.pattern);
        this.pattern = pattern;
    }

    /**
     * This is what you override when you extend DateFormat; use
     * {@link DateFormat#format(Date)} instead
     */
    @Override
    public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) {
        boolean includeTimeZone = pattern.includeTimeZone;
        if (pattern == Pattern.AUTO) {
            includeTimeZone = autoFormat(date);
        }
        super.format(date, toAppendTo, pos);
        if (includeTimeZone)
            convertRfc822TimeZoneToW3c(toAppendTo);
        return toAppendTo;
    }

    private boolean applyPattern(Pattern pattern) {
        applyPattern(pattern.pattern);
        return pattern.includeTimeZone;
    }

    private boolean autoFormat(Date date) {
        if (calendar == null)
            calendar = new GregorianCalendar();
        calendar.setTime(date);
        boolean hasMillis = calendar.get(MILLISECOND) > 0;
        if (hasMillis) {
            return applyPattern(Pattern.MILLISECOND);
        }
        boolean hasSeconds = calendar.get(SECOND) > 0;
        if (hasSeconds) {
            return applyPattern(Pattern.SECOND);
        }
        boolean hasTime = (calendar.get(HOUR_OF_DAY) + calendar.get(MINUTE)) > 0;
        if (hasTime) {
            return applyPattern(Pattern.MINUTE);
        }
        return applyPattern(Pattern.DAY);
    }

    /**
     * This is what you override when you extend DateFormat; use
     * {@link DateFormat#parse(String)} instead
     */
    @Override
    public Date parse(String text, ParsePosition pos) {
        text = convertW3cTimeZoneToRfc822(text);
        if (pattern == Pattern.AUTO) {
            return autoParse(text, pos);
        }
        return super.parse(text, pos);
    }

    private Date autoParse(String text, ParsePosition pos) {
        for (Pattern pattern : Pattern.values()) {
            if (pattern == Pattern.AUTO)
                continue;
            applyPattern(pattern);
            Date out = super.parse(text, pos);
            if (out != null)
                return out;
        }
        return null; // this will force a ParseException
    }

    private void convertRfc822TimeZoneToW3c(StringBuffer toAppendTo) {
        int length = toAppendTo.length();
        if (ZULU.equals(calendar.getTimeZone())) {
            toAppendTo.replace(length - 5, length, "Z");
        } else {
            toAppendTo.insert(length - 2, ':');
        }
    }

    private String convertW3cTimeZoneToRfc822(String source) {
        int length = source.length();
        if (source.endsWith("Z")) {
            return source.substring(0, length - 1) + "+0000";
        }
        if (source.charAt(length - 3) == ':') {
            return source.substring(0, length - 3) + source.substring(length - 2);
        }
        return source;
    }

}